From 7e9e7f79110e398a4d2b07de3093eaccd551f013 Mon Sep 17 00:00:00 2001 From: LogicParrot Date: Sat, 3 Sep 2016 18:38:29 +0300 Subject: Configurable dirty unused chunk cap to avoid RAM overuse (#3359) Configurable dirty unused chunk cap to avoid RAM overuse --- Server/Plugins/APIDump/APIDesc.lua | 1 + src/Chunk.cpp | 13 ++++++++++++ src/Chunk.h | 3 +++ src/ChunkMap.cpp | 21 +++++++++++++++++-- src/ChunkMap.h | 5 ++++- src/World.cpp | 42 +++++++++++++++++++++++++++++--------- src/World.h | 12 +++++++++-- 7 files changed, 82 insertions(+), 15 deletions(-) diff --git a/Server/Plugins/APIDump/APIDesc.lua b/Server/Plugins/APIDump/APIDesc.lua index 667723662..f61374407 100644 --- a/Server/Plugins/APIDump/APIDesc.lua +++ b/Server/Plugins/APIDump/APIDesc.lua @@ -2582,6 +2582,7 @@ end GetMinNetherPortalWidth = { Params = "", Return = "number", Notes = "Returns the minimum width for a nether portal" }, GetName = { Params = "", Return = "string", Notes = "Returns the name of the world, as specified in the settings.ini file." }, GetNumChunks = { Params = "", Return = "number", Notes = "Returns the number of chunks currently loaded." }, + GetNumUnusedDirtyChunks = { Params = "", Return = "number", Notes = "Returns the number of unused dirty chunks. That's the number of chunks that we can save and then unload." }, GetScoreBoard = { Params = "", Return = "{{cScoreBoard}}", Notes = "Returns the {{cScoreBoard|ScoreBoard}} object used by this world. " }, GetSeed = { Params = "", Return = "number", Notes = "Returns the seed of the world." }, GetSignLines = { Params = "BlockX, BlockY, BlockZ", Return = "IsValid, [Line1, Line2, Line3, Line4]", Notes = "Returns true and the lines of a sign at the specified coords, or false if there is no sign at the coords." }, diff --git a/src/Chunk.cpp b/src/Chunk.cpp index 06d5eb319..d833feea5 100644 --- a/src/Chunk.cpp +++ b/src/Chunk.cpp @@ -223,6 +223,19 @@ bool cChunk::CanUnload(void) +bool cChunk::CanUnloadAfterSaving(void) +{ + return + m_LoadedByClient.empty() && // The chunk is not used by any client + m_IsDirty && // The chunk is dirty + (m_StayCount == 0) && // The chunk is not in a ChunkStay + (m_Presence != cpQueued) ; // The chunk is not queued for loading / generating (otherwise multi-load / multi-gen could occur) +} + + + + + void cChunk::MarkSaving(void) { m_IsSaving = true; diff --git a/src/Chunk.h b/src/Chunk.h index 925680fdd..54e4a9502 100644 --- a/src/Chunk.h +++ b/src/Chunk.h @@ -112,6 +112,9 @@ public: bool CanUnload(void); + /** Returns true if the chunk could have been unloaded if it weren't dirty */ + bool CanUnloadAfterSaving(void); + bool IsLightValid(void) const {return m_IsLightValid; } /* diff --git a/src/ChunkMap.cpp b/src/ChunkMap.cpp index 7c4162b25..a16b08f15 100644 --- a/src/ChunkMap.cpp +++ b/src/ChunkMap.cpp @@ -2704,11 +2704,28 @@ void cChunkMap::SaveAllChunks(void) -int cChunkMap::GetNumChunks(void) +size_t cChunkMap::GetNumChunks(void) { cCSLock Lock(m_CSChunks); - return static_cast(m_Chunks.size()); // TODO: change return value to unsigned type + return m_Chunks.size(); +} + + + + +size_t cChunkMap::GetNumUnusedDirtyChunks(void) +{ + cCSLock Lock(m_CSChunks); + size_t res = 0; + for (const auto & Chunk : m_Chunks) + { + if (Chunk.second->IsValid() && Chunk.second->CanUnloadAfterSaving()) + { + res += 1; + } + } + return res; } diff --git a/src/ChunkMap.h b/src/ChunkMap.h index 328b0f74c..ff8f82f91 100644 --- a/src/ChunkMap.h +++ b/src/ChunkMap.h @@ -388,7 +388,10 @@ public: cWorld * GetWorld(void) { return m_World; } - int GetNumChunks(void); + size_t GetNumChunks(void); + + /** Returns the number of unused dirty chunks. Those are chunks that we can save and then unload */ + size_t GetNumUnusedDirtyChunks(void); void ChunkValidated(void); // Called by chunks that have become valid diff --git a/src/World.cpp b/src/World.cpp index c10cb52e9..d47d0832a 100644 --- a/src/World.cpp +++ b/src/World.cpp @@ -149,7 +149,7 @@ cWorld::cWorld(const AString & a_WorldName, eDimension a_Dimension, const AStrin m_WorldAge(0), m_TimeOfDay(0), m_LastTimeUpdate(0), - m_LastUnload(0), + m_LastChunkCheck(0), m_LastSave(0), m_SkyDarkness(0), m_GameMode(gmNotSet), @@ -453,6 +453,13 @@ void cWorld::Start(void) // The presence of a configuration value overrides everything // If no configuration value is found, GetDimension() is written to file and the variable is written to again to ensure that cosmic rays haven't sneakily changed its value m_Dimension = StringToDimension(IniFile.GetValueSet("General", "Dimension", DimensionToString(GetDimension()))); + int UnusedDirtyChunksCap = IniFile.GetValueSetI("General", "UnusedChunkCap", 1000); + if (UnusedDirtyChunksCap < 0) + { + UnusedDirtyChunksCap *= -1; + IniFile.SetValueI("General", "UnusedChunkCap", UnusedDirtyChunksCap); + } + m_UnusedDirtyChunksCap = static_cast(UnusedDirtyChunksCap); m_BroadcastDeathMessages = IniFile.GetValueSetB("Broadcasting", "BroadcastDeathMessages", true); m_BroadcastAchievementMessages = IniFile.GetValueSetB("Broadcasting", "BroadcastAchievementMessages", true); @@ -1057,16 +1064,22 @@ void cWorld::Tick(std::chrono::milliseconds a_Dt, std::chrono::milliseconds a_La TickWeather(static_cast(a_Dt.count())); - if (m_WorldAge - m_LastSave > std::chrono::minutes(5)) // Save each 5 minutes - { - SaveAllChunks(); - } - - if (m_WorldAge - m_LastUnload > std::chrono::seconds(10)) // Unload every 10 seconds + if (m_WorldAge - m_LastChunkCheck > std::chrono::seconds(10)) { + // Unload every 10 seconds UnloadUnusedChunks(); - } + if (m_WorldAge - m_LastSave > std::chrono::minutes(5)) + { + // Save every 5 minutes + SaveAllChunks(); + } + else if (GetNumUnusedDirtyChunks() > m_UnusedDirtyChunksCap) + { + // Save if we have too many dirty unused chunks + SaveAllChunks(); + } + } } @@ -2964,7 +2977,7 @@ bool cWorld::HasChunkAnyClients(int a_ChunkX, int a_ChunkZ) const void cWorld::UnloadUnusedChunks(void) { - m_LastUnload = std::chrono::duration_cast(m_WorldAge); + m_LastChunkCheck = std::chrono::duration_cast(m_WorldAge); m_ChunkMap->UnloadUnusedChunks(); } @@ -3578,7 +3591,7 @@ unsigned int cWorld::GetNumPlayers(void) -int cWorld::GetNumChunks(void) const +size_t cWorld::GetNumChunks(void) const { return m_ChunkMap->GetNumChunks(); } @@ -3587,6 +3600,15 @@ int cWorld::GetNumChunks(void) const +size_t cWorld::GetNumUnusedDirtyChunks(void) const +{ + return m_ChunkMap->GetNumUnusedDirtyChunks(); +} + + + + + void cWorld::GetChunkStats(int & a_NumValid, int & a_NumDirty, int & a_NumInLightingQueue) { m_ChunkMap->GetChunkStats(a_NumValid, a_NumDirty); diff --git a/src/World.h b/src/World.h index a00e181b6..a33831eb9 100644 --- a/src/World.h +++ b/src/World.h @@ -683,7 +683,10 @@ public: void ScheduleTask(int a_DelayTicks, std::function a_Task); /** Returns the number of chunks loaded */ - int GetNumChunks() const; // tolua_export + size_t GetNumChunks() const; // tolua_export + + /** Returns the number of unused dirty chunks. That's the number of chunks that we can save and then unload. */ + size_t GetNumUnusedDirtyChunks(void) const; // tolua_export /** Returns the number of chunks loaded and dirty, and in the lighting queue */ void GetChunkStats(int & a_NumValid, int & a_NumDirty, int & a_NumInLightingQueue); @@ -851,6 +854,11 @@ private: } ; + /** The maximum number of allowed unused dirty chunks for this world. + Loaded from config, enforced every 10 seconds by freeing some unused dirty chunks + if this was exceeded. */ + size_t m_UnusedDirtyChunksCap; + AString m_WorldName; /** The name of the overworld that portals in this world should link to. @@ -889,7 +897,7 @@ private: std::chrono::milliseconds m_WorldAge; std::chrono::milliseconds m_TimeOfDay; cTickTimeLong m_LastTimeUpdate; // The tick in which the last time update has been sent. - cTickTimeLong m_LastUnload; // The last WorldAge (in ticks) in which unloading was triggerred + cTickTimeLong m_LastChunkCheck; // The last WorldAge (in ticks) in which unloading and possibly saving was triggered cTickTimeLong m_LastSave; // The last WorldAge (in ticks) in which save-all was triggerred std::map m_LastSpawnMonster; // The last WorldAge (in ticks) in which a monster was spawned (for each megatype of monster) // MG TODO : find a way to optimize without creating unmaintenability (if mob IDs are becoming unrowed) -- cgit v1.2.3